/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.datatheorem.ohos.trustkit;


import ohos.app.Context;
import ohos.bundle.ApplicationInfo;
import ohos.global.resource.RawFileEntry;
import ohos.org.xml.sax.SAXException;
import ohos.system.version.SystemVersion;

import com.datatheorem.ohos.trustkit.ohad.Printer;
import com.datatheorem.ohos.trustkit.config.ConfigurationException;
import com.datatheorem.ohos.trustkit.config.TrustKitConfiguration;
import com.datatheorem.ohos.trustkit.pinning.TrustManagerBuilder;
import com.datatheorem.ohos.trustkit.reporting.BackgroundReporter;
import com.datatheorem.ohos.trustkit.utils.TrustKitLog;
import com.datatheorem.ohos.trustkit.utils.VendorIdentifier;
import com.datatheorem.ohos.trustkit.config.TrustKitConfiguration;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Set;
import java.util.logging.Logger;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class TrustKit {

    static TrustKit trustKitInstance;

    private final TrustKitConfiguration trustKitConfiguration;

    /**
     * 初始化
     *
     * @param context context
     * @param trustKitConfiguration trustKitConfiguration
     */
    TrustKit(Context context, TrustKitConfiguration trustKitConfiguration) {
        this.trustKitConfiguration = trustKitConfiguration;

        // Setup the debug-overrides setting if the App is debuggable
        // Do not use BuildConfig.DEBUG as it does not work for libraries

        boolean isAppDebuggable = context.getApplicationInfo().debug;
        Set<Certificate> debugCaCerts = null;
        boolean shouldOverridePins = false;
        if (isAppDebuggable) {
            debugCaCerts = trustKitConfiguration.getDebugCaCertificates();
            if (debugCaCerts != null) {
                TrustKitLog.i("App is debuggable - processing <debug-overrides> configuration.");
            }
            shouldOverridePins = trustKitConfiguration.shouldOverridePins();
        }

        // Create the background reporter for sending pin failure reports
        String appPackageName = context.getBundleName();
        String appVersion;
        if (null == SystemVersion.getVersion()) {
            appVersion = SystemVersion.getVersion();
        } else {
            appVersion = "N/A";
        }


        String appVendorId = VendorIdentifier.getOrCreate(context);
        BackgroundReporter reporter = new BackgroundReporter(context, appPackageName, appVersion,
            appVendorId);

        // Initialize the trust manager builder
        try {
            TrustManagerBuilder.initializeBaselineTrustManager(debugCaCerts,
                shouldOverridePins, reporter);
        } catch (CertificateException | NoSuchAlgorithmException | KeyStoreException
            | IOException e) {
            throw new ConfigurationException("Could not parse <debug-overrides> certificates");
        }
    }

    /**
     * Try to retrieve the Network Security Policy resource ID configured in the App's manifest.
     * <p>
     * Somewhat convoluted as other means of getting the resource ID involve using private APIs.
     *
     * @param context
     * @return The resource ID for the XML file containing the configured Network Security Policy or
     * -1 if no policy was configured in the App's manifest
     */
    static private int getNetSecConfigResourceId(Context context) {
        ApplicationInfo info = context.getApplicationInfo();

        class NetSecConfigResIdRetriever implements Printer {
            private int netSecConfigResourceId = -1;
            private final String NETSEC_LINE_FORMAT = "networkSecurityConfigRes=0x";

            public void println(String x) {
                if (netSecConfigResourceId == -1) {
                    // Attempt at parsing "networkSecurityConfigRes=0x1234"
                    if (x.contains(NETSEC_LINE_FORMAT)) {
                        netSecConfigResourceId = Integer.parseInt(x.substring(NETSEC_LINE_FORMAT.length()), 16);
                    }
                }
            }

            private int getNetworkSecurityConfigResId() {
                return netSecConfigResourceId;
            }
        }

        NetSecConfigResIdRetriever retriever = new NetSecConfigResIdRetriever();
        //TODO
        try {
            info.dump(String.valueOf(retriever), new PrintWriter(""));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return retriever.getNetworkSecurityConfigResId();
    }

    /**
     * 初始化
     *
     * @param context the application's context.
     * @return TrustKit
     * @throws ConfigurationException if the policy could not be parsed or contained errors.
     */
    public synchronized static TrustKit initializeWithNetworkSecurityConfiguration(Context context) {

        // Try to get the default network policy RawFileEntry
        String filePath = String.format("entry/resources/rawfile/%s", "network_security_config.xml");

        RawFileEntry networkSecurityConfig = context.getResourceManager().getRawFileEntry(filePath);
        try {
            Logger.getGlobal().warning("位置 = " + networkSecurityConfig.openRawFileDescriptor().toString());
        } catch (IOException e) {
            Logger.getGlobal().warning("位置 = " + e);
            e.printStackTrace();
        }

        return initializeWithNetworkSecurityConfiguration(context, networkSecurityConfig);
    }

    /**
     * 初始化
     *
     * @param context the application's context.
     * @param configurationResource the resource ID for the Network Security Configuration file to use.
     * @return TrustKit
     */

    public synchronized static TrustKit initializeWithNetworkSecurityConfiguration(
        Context context, RawFileEntry configurationResource) {
        if (trustKitInstance != null) {
            throw new IllegalStateException("TrustKit has already been initialized");
        }

        // Then try to load the supplied policy
        TrustKitConfiguration trustKitConfiguration = null;
        try {
            SAXReader reader = new SAXReader();

            reader.setFeature(context.getResourceManager().getElement(ResourceTable.String_xml_feature).getString(), true);
            Document document = reader.read(configurationResource.openRawFile());
            trustKitConfiguration = TrustKitConfiguration.fromXmlPolicy(
                context, document);
        } catch (CertificateException e) {
            throw new ConfigurationException("Could not find the debug certificate in the " +
                "network security police file");
        } catch (DocumentException | IOException e) {
            throw new ConfigurationException("Could not parse network security policy file" + e);
        } catch (Exception exception) {
            if (exception instanceof SAXException) {
                try {
                    throw new SAXException(exception);
                } catch (SAXException saxException) {
                    saxException.printStackTrace();
                }
            }
        }

        trustKitInstance = new TrustKit(context, trustKitConfiguration);
        return trustKitInstance;
    }

    /**
     * Retrieve the initialized instance of TrustKit.
     *
     * @return 返回一个TrustKit的单例对象
     * @throws IllegalStateException
     */
    public static TrustKit getInstance() {
        if (trustKitInstance == null) {
            throw new IllegalStateException("TrustKit has not been initialized");
        }
        return trustKitInstance;
    }

    /**
     * Retrieve the current TrustKit configuration.
     *
     * @return TrustKitConfiguration
     */
    public TrustKitConfiguration getConfiguration() {
        return trustKitConfiguration;
    }

    /**
     * Retrieve an {@code SSLSSocketFactory} that implements SSL pinning validation based on the
     * current TrustKit configuration for the specified serverHostname. It can be used with most
     * network APIs (such as {@code HttpsUrlConnection}) to add SSL pinning validation to the
     * connections.
     *
     * <p>
     * The {@code SSLSocketFactory} is configured for the supplied serverHostname, and will
     * enforce this domain's pinning policy even if a redirection to a different domain occurs
     * during the connection. Hence validation will always fail in the case of a redirection to
     * a different domain.
     * However, pinning validation is only meant to be used on the App's API server(s), and
     * redirections to other domains should not happen in this scenario.
     * </p>
     *
     * @param serverHostname the server's hostname that the {@code SSLSocketFactory} will be used to
     * connect to. This hostname will be used to retrieve the pinning policy
     * from the current TrustKit configuration.
     * @return SSLSocketFactory
     * @throws IllegalStateException
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     */
    public SSLSocketFactory getSSLSocketFactory(String serverHostname) {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{getTrustManager(serverHostname)}, null);
            return sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            e.printStackTrace();
            throw new IllegalStateException("Should not happen");
        }
    }


    /**
     * Retrieve an {@code X509TrustManager} that implements SSL pinning validation based on the
     * current TrustKit configuration for the supplied hostname. It can be used with some network
     * APIs that let developers supply a trust manager to customize SSL validation.
     *
     * <p>
     * The {@code X509TrustManager} is configured for the supplied serverHostname, and will
     * enforce this domain's pinning policy even if a redirection to a different domain occurs
     * during the connection. Hence validation will always fail in the case of a redirection to
     * a different domain.
     * However, pinning validation is only meant to be used on the App's API server(s), and
     * redirections to other domains should not happen in this scenario.
     * </p>
     *
     * @param serverHostname the server's hostname that the {@code X509TrustManager} will be used to
     * connect to. This hostname will be used to retrieve the pinning policy
     * from the current TrustKit configuration.
     * @return X509TrustManager
     */
    public X509TrustManager getTrustManager(String serverHostname) {
        return TrustManagerBuilder.getTrustManager(serverHostname);
    }

    public synchronized static void cleanTrustKit() {
        if (trustKitInstance != null) {
            trustKitInstance = null;
        }
        TrustManagerBuilder.clean();
    }
}

